# -*- coding: utf-8 -*-
import sys
if sys.getdefaultencoding() != 'utf-8':
    reload(sys)
    sys.setdefaultencoding('utf-8')
import xbmcswift2_patched as xbmcswift2
from xbmcswift2_patched import xbmc, xbmcplugin, xbmcgui, log
from traceback import print_exc
from xml.dom import minidom
from pprint import pprint
import xml, logging, time, os, threading, subprocess
import operator


# playlist, slideshow, play_and_wait (slideshow not implemented yet)
PLAY_MODE = 'playlist'


# Modify this:
BASE_DIR = '/PlayDir/'
FFMPEG_PARAM_VCODEC = '-vcodec'

def pre_init(basedir=None, log0=None):
    global BASE_DIR, FFMPEG_PARAM_VCODEC, PLAY_MODE, LOGO_DIR, STOP_FILE, MEDIA_DIR, SCHEDULE_DIR, MOVIE_DIR, IMAGE_DIR, IMAGE_VIDEO_DIR

    if basedir is None:
        basedir = BASE_DIR
        if not os.path.exists(basedir):
            basedir = '/Users/ulion/Develop/divisionmd/PlayDir/'
    #    PLAY_MODE = 'play_and_wait'
#            FFMPEG_PARAM_VCODEC = '-c:v'
    if not os.path.exists(basedir):
        raise Exception("un-existed BASE_DIR %s" % basedir)
    BASE_DIR = basedir

    # Custom logo dir so when no video/image displayed, display image in this dir.
    LOGO_DIR = os.path.join(BASE_DIR, "Logo")

    # if this file exists, only run the playlist in one loop
    STOP_FILE = os.path.join(BASE_DIR, "exitfile")


    MEDIA_DIR = os.path.join(BASE_DIR, '1.Media_Content')
    SCHEDULE_DIR = os.path.join(BASE_DIR, '3.Schedule')
    MOVIE_DIR = os.path.join(MEDIA_DIR, 'Movies')
    IMAGE_DIR = os.path.join(MEDIA_DIR, 'Images')
    # XXX: need make sure this dir is writable for playlist mode play pictures.
    IMAGE_VIDEO_DIR = IMAGE_DIR


    if log0 is not None:

        class StreamToLogger(object):
            """
            Fake file-like stream object that redirects writes to a logger instance.
            """
            def __init__(self, logger, name, log_level=logging.INFO):
                self.logger = logger
                self.name = name
                self.log_level = log_level
                self.linebuf = ''

            def write(self, buf):
                for line in buf.rstrip().split("\n"):
                    self.logger.log(self.log_level, '%s: %s' % (self.name, line.rstrip()))

        sl = StreamToLogger(log0, 'STDOUT', logging.DEBUG)
        ori_stdout = sys.stdout
        sys.stdout = sl
        sl = StreamToLogger(log0, 'STDERR', logging.ERROR)
        sys.stderr = sl

class XBMCPlayer( xbmc.Player ):
    """ Subclass of XBMC Player class.
        Overrides onplayback events, for custom actions.
    """
    def __init__( self, *args ):
        self.stopEvent = threading.Event()
        self.stopped = False

    def onPlayBackStarted( self ):
        # Will be called when xbmc starts playing a file
        log.debug("Playback started")
        self.stopEvent.clear()

    def onPlayBackEnded( self ):
        # Will be called when xbmc stops playing a file
        log.debug("End of Playback")
        self.stopEvent.set()

    def onPlayBackStopped( self ):
        # Will be called when user stops xbmc playing a file
        log.debug("Playback Stopped")
        self.stopEvent.set()
        self.stopped = True

if True or PLAY_MODE == 'play_and_wait':
    player = XBMCPlayer()
else:
    player = xbmc.Player()
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
is_playing = False

def playAndWait(filepath, duration=10, ispicture=False, label=None):
    log.debug('PLAY: %s (%s)' % (filepath, label))
    if ispicture:
        if duration is None:
            duration = 30
        xbmc.executebuiltin('ShowPicture(duration=%s,%s)' % (duration, filepath))
        log.debug('XXX: show picture sleep %s' % duration)
        xbmc.sleep(duration*1000)
        log.debug('YYY: after sleep %s' % duration)
    else:
        player.play(filepath)
        log.debug('XXX: play video sleep %s' % duration)
        xbmc.sleep(duration*1000)
        log.debug('YYY: after sleep %s' % duration)
        while not player.stopEvent.isSet() and not xbmc.abortRequested:
            xbmc.sleep(50)
        log.debug('ZZZ: got stop event set')
#        if duration is not None:
#            log.debug('XXX: sleep %s' % duration)
#            ret = player.stopEvent.wait(duration)
#            log.debug('YYY: after sleep %s, wait ret: %s' % (duration, ret))
#        else:
#            log.debug('XXX: wait stop event...')
#            player.stopEvent.wait()
#            log.debug('YYY: got stop event.')



def addToPlayList(item0):
    item = {}
    item['path'] = item0['path']
    item['label'] = item0['label']
    log.debug('Adding %s into playlist...' % str(item))
    item['info_type'] = 'video'
    item = xbmcswift2.ListItem.from_dict(**item)
    playlist.add(item.get_path(), item.as_xbmc_listitem())

def removeFromPlayList(posFrom, posTo=None):
    log.debug("removeFromPlayList(%s, %s) of %d, cur %d" % (posFrom, posTo, playlist.size(), playlist.getposition()))
    if posTo is None:
        posTo = posFrom
    for pos in range(posTo, posFrom - 1, -1):
        result = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "Playlist.Remove", "params": { "playlistid": 1, "position": %d }, "id": 1}' % pos)

def convert_image_to_video(imagepath, duration, videopath):
    if os.path.isfile(videopath) and os.path.getmtime(videopath) >= os.path.getmtime(imagepath):
        return True
    log.debug('CONVERT picture %s duration %s into %s' % (imagepath, duration, videopath))
    if duration is None:
        duration = 10
    else:
        duration = int(duration)
    try:
        cmdline = ['ffmpeg', '-loop', '1', '-y', '-r', '5', '-i', imagepath, FFMPEG_PARAM_VCODEC, 'libx264', '-preset', 'fast', '-threads', '0', '-shortest', '-t', str(duration), videopath]
        log.debug('ffmpeg conversion cmd: [%s]' % ' '.join(cmdline))
        ret = subprocess.call(cmdline)
        log.debug('ffmpeg conversion ret: %s' % ret)
        if ret != 0:
            log.warning('conversion cmd: [%s]' % ' '.join(cmdline))
            log.warning('conversion failed: %s' % ret)
            return False
        if not os.path.isfile(videopath):
            ret = subprocess.call(['ls', '-al', videopath])
            if not os.path.isfile(videopath):
                log.warning('conversion success but %s not exists?' % videopath)
                return False
        log.debug('result video file %s size: %d' % (videopath, os.path.getsize(videopath)))
        return True
    except OSError:
        print_exc()
        return False

def getText(node):
    nodelist = node.childNodes
    rc = []
    for node in nodelist:
        if node.nodeType == node.TEXT_NODE:
            rc.append(node.data)
    return ''.join(rc)

def getSubNodeText(node, subNodeName):
    subNodes = node.getElementsByTagName(subNodeName)
    if not subNodes:
        return None
    subNode = subNodes[0]
    return getText(subNode)

def processMediaItem(filename, label, mediaType, duration, mode):
    if mediaType not in ['Movies', 'Images']:
        log.debug('unsupported mediaType %s' % mediaType)
        return
    if mediaType == 'Movies':
        fullpath = os.path.join(MOVIE_DIR, filename)
    elif mediaType == 'Images':
        fullpath = os.path.join(IMAGE_DIR, filename)
    else:
        return
    log.debug('fullpath: ' + fullpath)
    if not os.path.isfile(fullpath):
        log.debug('not file: ' + fullpath)
        return
    item = {
        'path': fullpath,
        'label': label,
        'itemType': mediaType,
        'duration': duration,
    }
    return item

def playMediaItem(item, mode):
    global is_playing
    if mode == 'play_and_wait':
        playAndWait(item['path'], item['duration'], item['itemType'] == 'Images', item['label'])
    elif item['itemType'] == 'Images' and mode in ['playlist', 'convert_images']:
        if os.path.splitext(item['path'])[1].lower() != '.mp4':
            filename = os.path.basename(item['path'])
            duration = item['duration']
            videopath = os.path.join(IMAGE_VIDEO_DIR, '%s.%s.mp4' % (filename, duration))
            if not convert_image_to_video(item['path'], duration, videopath):
                return False
            item['path'] = videopath
        item['itemType'] = 'Movies'
    if mode == 'playlist':
        addToPlayList(item)
        if not is_playing or not (player.isPlaying() or PLAY_MODE == 'playlist' and playlist.getposition() >= 0):
            is_playing = True
            xbmc.executebuiltin("ActivateWindow(fullscreenvideo)")
            player.play(playlist)
    return True

def processPlayList(plst_file, mode):
    log.debug('processPlayList(%s, %s)' % (plst_file, mode))
    xmlDoc = minidom.parse(plst_file)
    topNode = xmlDoc.documentElement
    if topNode.nodeName != 'playlist':
        log.debug('invalid top node name: ' + topNode.nodeName)
        return

    flist = []
    for n in topNode.childNodes:
        if n.nodeType != n.ELEMENT_NODE:
            continue
        if n.nodeName != 'Video':
            continue
        for f in n.childNodes:
            fdict = {}
            for k in f.childNodes:
                if k.nodeType == n.ELEMENT_NODE and k.nodeName in ['name', 'label', 'duration', 'type', 'start']:
                    fdict[k.nodeName] = getText(k)
            if fdict and fdict.get('type') in ['Images', 'Movies']:
                flist.append(fdict)
    #flist.sort(key=operator.itemgetter('start'))
    flist.sort(key=lambda x: int(x['start']))
    pprint(flist)
    for f in flist:
        item = processMediaItem(f.get('name'), f.get('label'), f.get('type'), int(f.get('duration'))/1000, mode)
        if item:
            playMediaItem(item, mode)
    log.debug('processPlayList(%s, %s) finished.' % (plst_file, mode))

def playEventItem(item, mode):
    type = item['itemType']
    if type == 'group':
        processEvents(item['event'], mode)
    elif type == 'playlist':
        processPlayList(item['path'], mode)
    else:
        return playMediaItem(item, mode)
    return True

subgroup_current_item_dict = {}

def processEvents(node, mode):
    log.debug(node.toxml())
    events = []
    for n in node.childNodes:
        if n.nodeType != n.ELEMENT_NODE:
            continue
        log.debug(n.toxml())
        id = n.getAttribute('id')
        if n.getAttribute('active') == 'false':
            log.debug('skip inactive event ' + id)
            continue
        now = time.strftime('%Y/%m/%d %H:%M')
        # rrules="FREQ=CONTINUOUS" slotloop="false" recurring="false"
        rrules = n.getAttribute('rrules')
        recurring = n.getAttribute('recurring') == 'true'
        startTime = n.getAttribute('startTime')
        endTime = n.getAttribute('endTime')
        if rrules == "FREQ=CONTINUOUS" or not recurring:
            pass
        elif rrules.startswith('FREQ=DAILY'):
            until = rrules.find('UNTIL=')
            if until > 0:
                until = rrules[until+6:] + " 23:59"
                if now > until:
                    log.debug('daily event %s expires : %s >= %s' % (id, now, until))
                    continue
            if startTime:
                startTime = now[:10] + startTime[10:]
            if endTime:
                endTime = now[:10] + endTime[10:]
#            log.debug('rrules: [%s] start/end time: %s - %s' % (rrules, startTime, endTime))
        else:
            log.debug('unsupported rrules: [%s]' % rrules)
            
        if endTime and now > endTime:
            log.debug('event %s expires : %s >= %s' % (id, now, endTime))
            continue
        if startTime and now < startTime:
            log.debug('event %s not reach start time: %s < %s' % (id, now, startTime))
            continue
        if n.nodeName == 'group':
            item = {
                'id': id,
                'label': n.getAttribute('name'),
                'itemType': 'group',
                'event': n,
            }
            events.append(item)
            continue
        if n.nodeName != 'event':
            continue
        type = n.getAttribute('type')
        if type not in ['asset', 'playlist']:
            log.debug('event %s unsupported type: %s' % (id, type))
            continue
        filename = n.getAttribute('asset')
        if not filename:
            log.debug('event %s empty asset' % id)
            continue
        subtype = n.getAttribute('subtype')
        label = n.getAttribute('label')
        try:
            duration = int(n.getAttribute('duration'))
        except:
            duration = None
        if type == 'playlist':
            if not subtype:
                log.debug('event %s empty subtype for type playlist' % id)
                continue
            playlist_dir = os.path.join(BASE_DIR, subtype)
            if not os.path.isdir(playlist_dir):
                log.debug('event %s un-existed playlist dir %s' % (id, playlist_dir))
                continue
            fullpath = os.path.join(playlist_dir, filename)
            if not os.path.isfile(fullpath):
                if not os.path.isfile(fullpath + ".xml"):
                    log.debug('event %s un-existed playlist file %s' % (id, fullpath))
                    continue
                fullpath += ".xml"
            item = {
                'id': id,
                'path': fullpath,
                'label': label,
                'itemType': type,
                'duration': duration,
                'event': n,
            }
            events.append(item)
            continue
        if subtype not in ['Movies', 'Images']:
            log.debug('event %s unsupported subtype %s' % (id, subtype))
            continue
#        for attrName, attrValue in n.attributes.items():
#            log.debug('%s = %s' % (attrName, attrValue))
        item = processMediaItem(filename, label, subtype, duration, mode)
        if item:
            item['id'] = id
            item['event'] = n
            events.append(item)
    pprint(events)
    perLocked = node.getAttribute('locked')
    perUnlocked = node.getAttribute('unlocked')
    if node.nodeName == 'schedule' and perLocked and perUnlocked:
        log.debug('apply rule: locked: %s, unlocked: %s' % (perLocked, perUnlocked))
        perLocked = int(perLocked)
        perUnlocked = int(perUnlocked)
        if perLocked > 0 and perUnlocked > 0:
            lockedEvents = [item for item in events if item['event'].getAttribute('locked') == 'true']
            lockedEventsAlone = [item for item in lockedEvents if item['event'].getAttribute('playalone') == 'true']
            if lockedEventsAlone:
                lockedEvents = lockedEventsAlone
            unlockedEvents = [item for item in events if item['event'].getAttribute('locked') != 'true']
            unlockedEventsAlone = [item for item in unlockedEvents if item['event'].getAttribute('playalone') == 'true']
            if unlockedEventsAlone:
                unlockedEvents = unlockedEventsAlone
            if not lockedEvents or not unlockedEvents:
                for item in lockedEvents:
                    playEventItem(item, mode)
                for item in unlockedEvents:
                    playEventItem(item, mode)
                return
            lockedIndex = 0
            unlockedIndex = 0
            lockedAllPlayed = False
            unlockedAllPlayed = False
            while not lockedAllPlayed or not unlockedAllPlayed:
                for i in range(perLocked):
                    playEventItem(lockedEvents[lockedIndex], mode)
                    lockedIndex += 1
                    if lockedIndex >= len(lockedEvents):
                        lockedIndex = 0
                        lockedAllPlayed = True
                        if unlockedAllPlayed:
                            break
                if lockedAllPlayed and unlockedAllPlayed:
                    break
                for i in range(perUnlocked):
                    playEventItem(unlockedEvents[unlockedIndex], mode)
                    unlockedIndex += 1
                    if unlockedIndex >= len(unlockedEvents):
                        unlockedIndex = 0
                        unlockedAllPlayed = True
                        if lockedAllPlayed:
                            break
            return
    if node.nodeName == 'group':
        numPerSlot = node.getAttribute('numPerSlot')
        if numPerSlot:
            numPerSlot = int(numPerSlot)
            if numPerSlot > 0 and len(events) > 0:
                try:
                    ratio1 = int(node.getAttribute('ratio1'))
                except:
                    ratio1 = 0
                eventsPlayAlone = [item for item in events if item['event'].getAttribute('playalone') == 'true']
                log.debug('apply numPerSlot %s ratio1 %s to group [%s], alone [%s/%s]' % (numPerSlot, ratio1, node.getAttribute('name'), len(eventsPlayAlone), len(events)))
                group_key = (node.getAttribute('id'), node.getAttribute('name'))
                (slotIndex, currentItemIndex) = subgroup_current_item_dict.get(group_key, (0, 0))
                if eventsPlayAlone:
                    numToPlay = numPerSlot
                    while numToPlay > 0:
                        item = events[currentItemIndex]
                        currentItemIndex = (currentItemIndex + 1) % len(events)
                        if item['event'].getAttribute('playalone') == 'true':
                            playEventItem(item, mode)
                            numToPlay -= 1
                    subgroup_current_item_dict[group_key] = (slotIndex, currentItemIndex)
                    return
                if ratio1 > 0:
                    slotLoopEvents = [item for item in events if item['event'].getAttribute('slotloop') == 'true']
                    if len(slotLoopEvents) < len(events) and len(slotLoopEvents) > 0:
                        numToPlay = numPerSlot
                        if slotIndex == 0:
                            slotLoopPlayNum = min(numPerSlot, len(slotLoopEvents))
                            for i in range(slotLoopPlayNum):
                                playEventItem(slotLoopEvents[i], mode)
                            numToPlay -= slotLoopPlayNum
                        slotIndex = (slotIndex + 1) % ratio1
                        while numToPlay > 0:
                            item = events[currentItemIndex]
                            currentItemIndex = (currentItemIndex + 1) % len(events)
                            if item['event'].getAttribute('slotloop') != 'true':
                                playEventItem(item, mode)
                                numToPlay -= 1
                        subgroup_current_item_dict[group_key] = (slotIndex, currentItemIndex)
                        return
                for i in range(numPerSlot):
                    playEventItem(events[(currentItemIndex + i) % len(events)], mode)
                currentItemIndex += numPerSlot
                currentItemIndex %= len(events)
                subgroup_current_item_dict[group_key] = (0, currentItemIndex)
                return
    eventsPlayAlone = [item for item in events if item['event'].getAttribute('playalone') == 'true']
    if eventsPlayAlone:
        events = eventsPlayAlone
    for item in events:
        playEventItem(item, mode)

def run_schedule(schfile, mode=PLAY_MODE):
    log.debug('run_schedule(%s, %s)' % (schfile, mode))
    xmlDoc = minidom.parse(schfile)
    topNode = xmlDoc.documentElement
    if topNode.nodeName != 'schedule':
        raise 'invalid top node name: ' + topNode.nodeName
    if mode == 'playlist':
        playlist.clear()
        xbmc.executebuiltin("PlayerControl(RepeatOff)")
    elif mode == 'playlist_preload':
        mode = "playlist"
    processEvents(topNode, mode)
    log.debug('run_schedule(%s, %s) finished.' % (schfile, mode))
            
    
def run(schfile = None):
    if schfile is None:
        schfile = os.path.join(SCHEDULE_DIR, '__schedule.sch')
    log.debug('schedule file: ' + schfile)
#        xbmc.executebuiltin("ActivateWindow(screensaver)")
#        xbmc.executebuiltin("ActivateWindow(fullscreenvideo)")
    if os.path.exists(LOGO_DIR):
        xbmc.executebuiltin("SlideShow(%s)" % LOGO_DIR)
    while not xbmc.abortRequested and not os.path.exists(STOP_FILE) and not player.stopped:
#            time.sleep(10)
        try:
            oldmtime = os.path.getmtime(schfile)
            run_schedule(schfile)
            playlist_size = playlist.size()
            log.debug('schedule playlist size: %d' % playlist_size)
            while not xbmc.abortRequested and (player.isPlaying() or PLAY_MODE == 'playlist' and playlist.getposition() >= 0):
                try:
                    if oldmtime < os.path.getmtime(schfile):
                        log.debug('DETECTED: schedule file updated')
                        if PLAY_MODE == 'playlist':
                            oldmtime = os.path.getmtime(schfile)
                            run_schedule(schfile, 'convert_images')
                        if playlist.getposition() < playlist_size:
                            log.debug('clean up preloaded next loop round playlist')
                            removeFromPlayList(playlist_size, playlist.size() - 1)
                    if PLAY_MODE == 'playlist':
                        if playlist.getposition() >= playlist_size:
                            log.debug('clean up last loop round playlist')
                            removeFromPlayList(0, playlist_size - 1)
                            playlist_size = playlist.size()

                        # preload playlist while the last 2 items playing
                        if playlist.getposition() >= 0 and playlist_size == playlist.size() and playlist.size() - playlist.getposition() <= 2:
                            log.debug('preload next round playlist')
                            # will append new round items into the playlist.
                            run_schedule(schfile, "playlist_preload")
                except:
                    print_exc()
                xbmc.sleep(50)
            is_playing = False
            if xbmc.abortRequested:
                log.debug('XBMC ABORTING')
            else:
                log.debug('PLAYLIST ENDED')
        except:
            print_exc()
            xbmc.sleep(1000)
#    if not xbmc.abortRequested:
#        xbmc.executebuiltin("ActivateWindow(home)")

if __name__ == '__main__':
    try:
        log.debug('argv: ' + repr(sys.argv))
        pre_init(log0=xbmcswift2.logger.log)
        run()
    except:
        print_exc()
    log.debug('DMDS-XBMC schedule service exited.')
